/**
 * @license
 * Copyright 2020 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */

import {PbAesCtrHmacAeadKey, PbAesCtrHmacAeadKeyFormat, PbAesCtrKey, PbAesCtrKeyFormat, PbAesCtrParams, PbHashType, PbHmacKey, PbHmacKeyFormat, PbHmacParams, PbKeyData} from '../internal/proto';
import {bytesLength} from '../internal/proto_shims';
import * as Random from '../subtle/random';
import {assertExists} from '../testing/internal/test_utils';

import {AesCtrHmacAeadKeyManager} from './aes_ctr_hmac_aead_key_manager';
import {Aead} from './internal/aead';

const KEY_TYPE = 'type.googleapis.com/google.crypto.tink.AesCtrHmacAeadKey';
const VERSION = 0;

/////////////////////////////////////////////////////////////////////////////
// Helper functions for tests

/** creates new AesCtrHmacAeadKeyFormat with allowed parameters */
function createTestKeyFormat(): PbAesCtrHmacAeadKeyFormat {
  const KEY_SIZE = 16;
  const IV_SIZE = 12;
  const TAG_SIZE = 16;


  const keyFormat = new PbAesCtrHmacAeadKeyFormat();
  const aesCtrKeyFormat = new PbAesCtrKeyFormat();
  aesCtrKeyFormat.setKeySize(KEY_SIZE);
  const aesCtrParams = new PbAesCtrParams();
  aesCtrParams.setIvSize(IV_SIZE);
  aesCtrKeyFormat.setParams(aesCtrParams);
  keyFormat.setAesCtrKeyFormat(aesCtrKeyFormat);

  // set HMAC key
  const hmacKeyFormat = new PbHmacKeyFormat();
  hmacKeyFormat.setKeySize(KEY_SIZE);
  const hmacParams = new PbHmacParams();
  hmacParams.setHash(PbHashType.SHA1);
  hmacParams.setTagSize(TAG_SIZE);
  hmacKeyFormat.setParams(hmacParams);
  keyFormat.setHmacKeyFormat(hmacKeyFormat);

  return keyFormat;
}

/** creates new AesCtrHmacAeadKey with allowed parameters */
function createTestKey(): PbAesCtrHmacAeadKey {
  const KEY_SIZE = 16;
  const IV_SIZE = 12;
  const TAG_SIZE = 16;


  const key = new PbAesCtrHmacAeadKey();
  key.setVersion(0);
  const aesCtrKey = new PbAesCtrKey();
  aesCtrKey.setVersion(0);
  const aesCtrParams = new PbAesCtrParams();
  aesCtrParams.setIvSize(IV_SIZE);
  aesCtrKey.setParams(aesCtrParams);
  aesCtrKey.setKeyValue(Random.randBytes(KEY_SIZE));
  key.setAesCtrKey(aesCtrKey);

  // set HMAC key
  const hmacKey = new PbHmacKey();
  hmacKey.setVersion(0);
  const hmacParams = new PbHmacParams();
  hmacParams.setHash(PbHashType.SHA1);
  hmacParams.setTagSize(TAG_SIZE);
  hmacKey.setParams(hmacParams);
  hmacKey.setKeyValue(Random.randBytes(KEY_SIZE));
  key.setHmacKey(hmacKey);

  return key;
}

/** creates new PbKeyData with allowed parameters */
function createTestKeyData(): PbKeyData {
  const keyData = new PbKeyData()
                      .setTypeUrl(KEY_TYPE)
                      .setValue(createTestKey().serializeBinary())
                      .setKeyMaterialType(PbKeyData.KeyMaterialType.SYMMETRIC);
  return keyData;
}

describe('aes ctr hmac aead key manager test', function() {
  /////////////////////////////////////////////////////////////////////////////
  // tests for newKey method

  // newKey method -- key formats
  it('new key bad key format', async function() {
    const keyFormat = new PbAesCtrKeyFormat();
    const manager = new AesCtrHmacAeadKeyManager();

    try {
      manager.getKeyFactory().newKey(keyFormat);
    } catch (e: any) {
      expect(e.toString())
          .toBe('SecurityException: Expected AesCtrHmacAeadKeyFormat-proto');
      return;
    }
    fail('An exception should be thrown.');
  });

  it('new key bad serialized key', async function() {
    // this is not a serialized key format
    const serializedKeyFormat = new Uint8Array(4);
    const manager = new AesCtrHmacAeadKeyManager();

    try {
      manager.getKeyFactory().newKey(serializedKeyFormat);
    } catch (e: any) {
      expect(e.toString())
          .toBe(
              'SecurityException: Could not parse the given Uint8Array as a serialized' +
              ' proto of ' + KEY_TYPE);
      return;
    }
    fail('An exception should be thrown.');
  });

  // newKey method -- bad parametrs of AES CTR KEY format
  it('new key not supported aes ctr key size', async function() {
    const keySize: number = 11;
    const manager = new AesCtrHmacAeadKeyManager();

    const keyFormat = createTestKeyFormat();
    keyFormat.getAesCtrKeyFormat()?.setKeySize(keySize);

    try {
      manager.getKeyFactory().newKey(keyFormat);
    } catch (e: any) {
      expect(e.toString())
          .toBe(
              'InvalidArgumentsException: unsupported AES key size: ' +
              keySize);
      return;
    }
    fail('An exception should be thrown.');
  });
  it('new key iv size out of range', async function() {
    const ivSizeOutOfRange: number[] = [10, 18];
    const manager = new AesCtrHmacAeadKeyManager();

    const keyFormat = createTestKeyFormat();

    const ivSizeOutOfRangeLength = ivSizeOutOfRange.length;
    for (let i = 0; i < ivSizeOutOfRangeLength; i++) {
      keyFormat.getAesCtrKeyFormat()?.getParams()?.setIvSize(
          ivSizeOutOfRange[i]);
      try {
        manager.getKeyFactory().newKey(keyFormat);
      } catch (e: any) {
        expect(e.toString())
            .toBe(
                'SecurityException: Invalid AES CTR HMAC key format: IV size is ' +
                'out of range: ' + ivSizeOutOfRange[i]);
        continue;
      }
      fail('An exception should be thrown.');
    }
  });

  // newKey method -- bad parametrs of HMAC KEY format
  it('new key small hmac key size', async function() {
    const keySize: number = 11;
    const manager = new AesCtrHmacAeadKeyManager();

    const keyFormat = createTestKeyFormat();
    keyFormat.getHmacKeyFormat()?.setKeySize(keySize);

    try {
      manager.getKeyFactory().newKey(keyFormat);
    } catch (e: any) {
      expect(e.toString())
          .toBe(
              'SecurityException: Invalid AES CTR HMAC key format: HMAC key is' +
              ' too small: ' + keySize);
      return;
    }
    fail('An exception should be thrown.');
  });

  it('new key hash type unsupported', async function() {
    const manager = new AesCtrHmacAeadKeyManager();

    const keyFormat = createTestKeyFormat();
    keyFormat.getHmacKeyFormat()?.getParams()?.setHash(PbHashType.UNKNOWN_HASH);

    try {
      manager.getKeyFactory().newKey(keyFormat);
    } catch (e: any) {
      expect(e.toString()).toBe('SecurityException: Unknown hash type.');
      return;
    }
    fail('An exception should be thrown.');
  });

  it('new key small tag size', async function() {
    const SMALL_TAG_SIZE = 8;
    const manager = new AesCtrHmacAeadKeyManager();

    const keyFormat = createTestKeyFormat();
    keyFormat.getHmacKeyFormat()?.getParams()?.setTagSize(SMALL_TAG_SIZE);

    try {
      manager.getKeyFactory().newKey(keyFormat);
    } catch (e: any) {
      expect(e.toString())
          .toBe(
              'SecurityException: Invalid HMAC params: tag size ' +
              SMALL_TAG_SIZE + ' is too small.');
      return;
    }
    fail('An exception should be thrown.');
  });

  it('new key big tag size for hash type', async function() {
    const tagSizes = [
      {'hashType': PbHashType.SHA1, 'tagSize': 22},
      {'hashType': PbHashType.SHA256, 'tagSize': 34},
      {'hashType': PbHashType.SHA512, 'tagSize': 66},
    ];
    const manager = new AesCtrHmacAeadKeyManager();

    const keyFormat = createTestKeyFormat();

    const tagSizesLength = tagSizes.length;
    for (let i = 0; i < tagSizesLength; i++) {
      keyFormat.getHmacKeyFormat()?.getParams()?.setHash(
          tagSizes[i]['hashType']);
      keyFormat.getHmacKeyFormat()?.getParams()?.setTagSize(
          tagSizes[i]['tagSize']);
      try {
        manager.getKeyFactory().newKey(keyFormat);
      } catch (e: any) {
        expect(e.toString())
            .toBe(
                'SecurityException: Invalid HMAC params: tag size ' +
                tagSizes[i]['tagSize'] + ' is out of range.');
        continue;
      }
      fail('An exception should be thrown.');
    }
  });

  it('new key via format proto', async function() {
    const manager = new AesCtrHmacAeadKeyManager();

    const keyFormat = createTestKeyFormat();

    const key = manager.getKeyFactory().newKey(keyFormat);

    // testing AES CTR key
    expect(bytesLength(key.getAesCtrKey()?.getKeyValue()))
        .toBe(keyFormat.getAesCtrKeyFormat()?.getKeySize());
    expect(key.getAesCtrKey()?.getVersion()).toBe(0);
    expect(key.getAesCtrKey()?.getParams()?.getIvSize())
        .toBe(keyFormat.getAesCtrKeyFormat()?.getParams()?.getIvSize());

    // testing HMAC key
    expect(bytesLength(key.getHmacKey()?.getKeyValue()))
        .toBe(keyFormat.getHmacKeyFormat()?.getKeySize());
    expect(key.getHmacKey()?.getVersion()).toBe(0);
    expect(key.getHmacKey()?.getParams()?.getHash())
        .toBe(keyFormat.getHmacKeyFormat()?.getParams()?.getHash());
    expect(key.getHmacKey()?.getParams()?.getTagSize())
        .toBe(keyFormat.getHmacKeyFormat()?.getParams()?.getTagSize());
  });

  it('new key via serialized format proto', async function() {
    const manager = new AesCtrHmacAeadKeyManager();

    const keyFormat = createTestKeyFormat();
    const serializedKeyFormat = keyFormat.serializeBinary();

    const key = manager.getKeyFactory().newKey(serializedKeyFormat);

    // testing AES CTR key
    expect(bytesLength(key.getAesCtrKey()?.getKeyValue()))
        .toBe(keyFormat.getAesCtrKeyFormat()?.getKeySize());
    expect(key.getAesCtrKey()?.getVersion()).toBe(0);
    expect(key.getAesCtrKey()?.getParams()?.getIvSize())
        .toBe(keyFormat.getAesCtrKeyFormat()?.getParams()?.getIvSize());


    // testing HMAC key
    expect(bytesLength(key.getHmacKey()?.getKeyValue()))
        .toBe(keyFormat.getHmacKeyFormat()?.getKeySize());
    expect(key.getHmacKey()?.getVersion()).toBe(0);
    expect(key.getHmacKey()?.getParams()?.getHash())
        .toBe(keyFormat.getHmacKeyFormat()?.getParams()?.getHash());
    expect(key.getHmacKey()?.getParams()?.getTagSize())
        .toBe(keyFormat.getHmacKeyFormat()?.getParams()?.getTagSize());
  });

  /////////////////////////////////////////////////////////////////////////////
  // tests for NewKeyData method

  it('new key data bad serialized key', async function() {
    const serializedKeyFormats = [new Uint8Array(1), new Uint8Array(0)];
    const aeadKeyManager = new AesCtrHmacAeadKeyManager();

    const serializedKeyFormatsLength = serializedKeyFormats.length;
    for (let i = 0; i < serializedKeyFormatsLength; i++) {
      try {
        aeadKeyManager.getKeyFactory().newKeyData(serializedKeyFormats[i]);
      } catch (e: any) {
        expect(e.toString())
            .toBe(
                'SecurityException: Could not parse the given Uint8Array as a ' +
                'serialized proto of ' + KEY_TYPE);
        continue;
      }
      fail(
          'An exception should be thrown for the string: ' +
          serializedKeyFormats[i]);
    }
  });

  it('new key data from valid key', async function() {
    const keyFormat = createTestKeyFormat();
    const serializedKeyFormat = keyFormat.serializeBinary();
    const manager = new AesCtrHmacAeadKeyManager();

    const keyData = manager.getKeyFactory().newKeyData(serializedKeyFormat);

    expect(keyData.getTypeUrl()).toBe(KEY_TYPE);
    expect(keyData.getKeyMaterialType())
        .toBe(PbKeyData.KeyMaterialType.SYMMETRIC);

    const key = PbAesCtrHmacAeadKey.deserializeBinary(keyData.getValue());

    expect(bytesLength(key.getAesCtrKey()?.getKeyValue()))
        .toBe(keyFormat.getAesCtrKeyFormat()?.getKeySize());
    expect(bytesLength(key.getHmacKey()?.getKeyValue()))
        .toBe(keyFormat.getHmacKeyFormat()?.getKeySize());
  });

  /////////////////////////////////////////////////////////////////////////////
  // tests for getPrimitive method

  it('get primitive unsupported key data type', async function() {
    const aeadKeyManager = new AesCtrHmacAeadKeyManager();
    const keyData = createTestKeyData().setTypeUrl('bad type url');

    try {
      await aeadKeyManager.getPrimitive(Aead, keyData);
    } catch (e: any) {
      expect(e.toString())
          .toBe(
              'SecurityException: Key type ' + keyData.getTypeUrl() +
              ' is not supported. This key manager supports ' + KEY_TYPE + '.');
      return;
    }
    fail('An exception should be thrown');
  });

  it('get primitive unsupported key type', async function() {
    const aeadKeyManager = new AesCtrHmacAeadKeyManager();
    const key = new PbAesCtrKey();

    try {
      await aeadKeyManager.getPrimitive(Aead, key);
    } catch (e: any) {
      expect(e.toString())
          .toBe(
              'SecurityException: Given key type is not supported. ' +
              'This key manager supports ' + KEY_TYPE + '.');
      return;
    }
    fail('An exception should be thrown');
  });

  it('get primitive bad version', async function() {
    const version = 1;
    const aeadKeyManager = new AesCtrHmacAeadKeyManager();
    const key: PbAesCtrHmacAeadKey = createTestKey();

    key.getAesCtrKey()?.setVersion(version);

    try {
      await aeadKeyManager.getPrimitive(Aead, key);
    } catch (e: any) {
      expect(e.toString())
          .toBe(
              'SecurityException: Version is out of bound, must be between 0 ' +
              'and ' + VERSION + '.');
      return;
    }
    fail('An exception should be thrown');
  });

  it('get primitive short aes ctr key', async function() {
    const keySize = 5;
    const aeadKeyManager = new AesCtrHmacAeadKeyManager();
    const key: PbAesCtrHmacAeadKey = createTestKey();

    key.getAesCtrKey()?.setKeyValue(new Uint8Array(keySize));

    try {
      await aeadKeyManager.getPrimitive(Aead, key);
    } catch (e: any) {
      expect(e.toString())
          .toBe(
              'InvalidArgumentsException: unsupported AES key size: ' +
              keySize);
      return;
    }
    fail('An exception should be thrown');
  });

  it('get primitive aes ctr key small iv size', async function() {
    const ivSizeOutOfRange: number[] = [9, 19];
    const manager = new AesCtrHmacAeadKeyManager();
    const key: PbAesCtrHmacAeadKey = createTestKey();

    const ivSizeOutOfRangeLength = ivSizeOutOfRange.length;
    for (let i = 0; i < ivSizeOutOfRangeLength; i++) {
      key.getAesCtrKey()?.getParams()?.setIvSize(ivSizeOutOfRange[i]);
      try {
        await manager.getPrimitive(Aead, key);
      } catch (e: any) {
        expect(e.toString())
            .toBe(
                'SecurityException: Invalid AES CTR HMAC key format: IV size is ' +
                'out of range: ' + ivSizeOutOfRange[i]);
        continue;
      }
      fail('An exception should be thrown.');
    }
  });

  it('get primitive short hmac key', async function() {
    const keySize = 5;
    const aeadKeyManager = new AesCtrHmacAeadKeyManager();
    const key: PbAesCtrHmacAeadKey = createTestKey();

    key.getHmacKey()?.setKeyValue(new Uint8Array(keySize));

    try {
      await aeadKeyManager.getPrimitive(Aead, key);
    } catch (e: any) {
      expect(e.toString())
          .toBe(
              'SecurityException: Invalid AES CTR HMAC key format: HMAC key is' +
              ' too small: ' + keySize);
      return;
    }
    fail('An exception should be thrown');
  });

  it('get primitive hmac key unsupported hash type', async function() {
    const aeadKeyManager = new AesCtrHmacAeadKeyManager();
    const key: PbAesCtrHmacAeadKey = createTestKey();

    key.getHmacKey()?.getParams()?.setHash(PbHashType.UNKNOWN_HASH);

    try {
      await aeadKeyManager.getPrimitive(Aead, key);
    } catch (e: any) {
      expect(e.toString()).toBe('SecurityException: Unknown hash type.');
      return;
    }
    fail('An exception should be thrown');
  });

  it('get primitive hmac key small tag size', async function() {
    const SMALL_TAG_SIZE = 9;
    const aeadKeyManager = new AesCtrHmacAeadKeyManager();
    const key: PbAesCtrHmacAeadKey = createTestKey();

    key.getHmacKey()?.getParams()?.setTagSize(SMALL_TAG_SIZE);

    try {
      await aeadKeyManager.getPrimitive(Aead, key);
    } catch (e: any) {
      expect(e.toString())
          .toBe(
              'SecurityException: Invalid HMAC params: tag size ' +
              SMALL_TAG_SIZE + ' is too small.');
      return;
    }
    fail('An exception should be thrown');
  });

  it('get primitive hmac big tag size', async function() {
    const tagSizes = [
      {'hashType': PbHashType.SHA1, 'tagSize': 22},
      {'hashType': PbHashType.SHA256, 'tagSize': 34},
      {'hashType': PbHashType.SHA512, 'tagSize': 66},
    ];
    const manager = new AesCtrHmacAeadKeyManager();

    const key: PbAesCtrHmacAeadKey = createTestKey();

    const tagSizesLength = tagSizes.length;
    for (let i = 0; i < tagSizesLength; i++) {
      const params = assertExists(key.getHmacKey()?.getParams());
      params.setHash(tagSizes[i]['hashType']);
      params.setTagSize(tagSizes[i]['tagSize']);
      try {
        await manager.getPrimitive(Aead, key);
      } catch (e: any) {
        expect(e.toString())
            .toBe(
                'SecurityException: Invalid HMAC params: tag size ' +
                tagSizes[i]['tagSize'] + ' is out of range.');
        continue;
      }
      fail('An exception should be thrown.');
    }
  });

  // tests for getting primitive from valid key/keyData
  it('get primitive from key', async function() {
    const aeadKeyManager = new AesCtrHmacAeadKeyManager();
    const key = createTestKey();
    const plaintext = Random.randBytes(8);
    const aad = Random.randBytes(8);

    const primitive: Aead = await aeadKeyManager.getPrimitive(Aead, key);
    const ciphertext = await primitive.encrypt(plaintext, aad);
    const decryptedCiphertext = await primitive.decrypt(ciphertext, aad);

    expect(decryptedCiphertext).toEqual(plaintext);
  });

  it('get primitive from key data', async function() {
    const aeadKeyManager = new AesCtrHmacAeadKeyManager();
    const keyData = createTestKeyData();
    const plaintext = Random.randBytes(8);
    const aad = Random.randBytes(8);

    const primitive: Aead = await aeadKeyManager.getPrimitive(Aead, keyData);
    const ciphertext = await primitive.encrypt(plaintext, aad);
    const decryptedCiphertext = await primitive.decrypt(ciphertext, aad);

    expect(decryptedCiphertext).toEqual(plaintext);
  });

  /////////////////////////////////////////////////////////////////////////////
  // tests for getVersion, getKeyType and doesSupport methods

  it('get version should be zero', async function() {
    const manager = new AesCtrHmacAeadKeyManager();
    expect(manager.getVersion()).toBe(0);
  });

  it('get key type should be aes ctr hmac aead key', async function() {
    const manager = new AesCtrHmacAeadKeyManager();
    expect(manager.getKeyType()).toBe(KEY_TYPE);
  });

  it('does support should support aes ctr hmac aead key', async function() {
    const manager = new AesCtrHmacAeadKeyManager();
    expect(manager.doesSupport(KEY_TYPE)).toBe(true);
  });

  it('get primitive type should be aead', async function() {
    const manager = new AesCtrHmacAeadKeyManager();
    expect(manager.getPrimitiveType()).toBe(Aead);
  });
});
